from __future__ import print_function
from __future__ import absolute_import

import stackless
import asyncore
import socket
import struct
import logging
import cStringIO as cstringio
import cPickle as cpickle
import collections
import time
import weakref

from . import core
PT_NONE = 0
PT_MESSAGE = 1
BUFFER_SIZE = 0x2000
BUFFER_SIZE_MAX = 0xFFFF
DEBUG_OUT_BUFFER = 0
DEBUG_IN_BUFFER = 1
MAX_CHANNELS = 1024

GLogger = None
GConnections = []

def RegisterConnection(conn):
    GConnections.append(weakref.ref(conn, OnConnectionDel))

def OnConnectionDel(connref):
    GConnections.remove(connref)

class CNetService(core.CTaskletService):
    Name = 'Network Service'
    Depends = []
    
    def __init__(self, *args, **kwds):
        core.CTaskletService.__init__(self, *args, **kwds)

    def TaskletMain(self):
        global GLogger

        GLogger = logging.getLogger('net')
        sleep = core.Sleep
        loop = asyncore.loop
        try:
            while 1:
                loop(timeout=0.0, count=1)
                for wr in GConnections:
                    conn = wr()
                    assert conn is not None
                    conn.Dispatch()
                sleep(0.020)
        finally:
            GLogger = None    

class CNetConnection(asyncore.dispatcher):
    def __init__(self, host=None, port=None, sock=None, channels=MAX_CHANNELS):
        asyncore.dispatcher.__init__(self, sock)
        if sock is None:
            GLogger.debug('created NetConnection without socket')
            GLogger.debug('host=%s, port=%s', host, port)
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect((host, port))
            self.Host = host
            self.Port = port
        else:
            GLogger.debug('created NetConnection with socket')
            GLogger.debug('host=%s, port=%s', self.addr[0], self.addr[1])
            self.Host = self.addr[0]
            self.Port = self.addr[1]
        self.OutBuffer = cstringio.StringIO()
        self.InPackets = collections.deque()
        self.InBuffer = cstringio.StringIO()
        self.LastRecvTime = 0
        self.LastSendTime = 0
        chanlist = []
        for i in xrange(channels):
            chanlist.append([collections.deque(), None])
        self.Channels = chanlist
        self.DebugTruncing = 0
        RegisterConnection(self)

    def handle_read(self):
        data = self.recv(8192)
        if len(data) > 0:
            GLogger.debug('received %s bytes of data', len(data))
            self.LastRecvTime = time.time()
            self.InBuffer.write(data)
            self.OnReceivedData()
            
    def handle_write(self):
        outbuf = self.OutBuffer
        outbuf.seek(0)
        sent = self.send(outbuf.read())
        if sent > 0:
            outbuf.seek(sent)
            remains = outbuf.read()
            outbuf.seek(0)
            outbuf.write(remains)

        self.LastSendTime = time.time()
        GLogger.debug('sent %s bytes of data', sent)

    def handle_connect(self):
        self.OnConnected()

    def handle_close(self):
        self.OnClosed()
        self.close()

    def writable(self):
        return self.OutBuffer.tell() > 0

    def OnSentData(self):
        pass

    def OnReceivedData(self):
        '''
        Reads data from the InBuffer and places the unpickled
        packets into InPackets.
        '''
        buf = self.InBuffer
        bufsize = buf.tell()
        start = 0
        # Packet header is 5 bytes
        while bufsize - start > 5:
            buf.seek(start)
            packsize, packchan, packop = struct.unpack('HHB', buf.read(5))
            if bufsize < packsize + 5:
                GLogger.debug('packet read delayed, haven\'t received whole packet')
                GLogger.debug('bufsize=%s, packsize=%s, packchan=%s, packop=%s', bufsize, packsize, packchan, packop)
                buf.seek(start)
                return
            packet = buf.read(packsize)
            assert len(packet) == packsize
            packet = cpickle.loads(packet)
            self.InPackets.append((packchan, packet))
            GLogger.debug('received packet on channel %s: %s', packchan, packet)
            start += packsize + 5
        else:
            if start > 0:
                remains = buf.read()
                buf.seek(0)
                buf.write(remains)

    def Dispatch(self):
        '''
        Sorts incomming packets into their channels and
        dispatchers packets to their receivers.
        '''
        channels = self.Channels
        inpacks = self.InPackets
        # Send incoming packets to their channels
        while len(inpacks) > 0:
            packchan, packet = inpacks.popleft()
            channels[packchan][0].append(packet)
        # Dispatchs packets to receivers
        for channel in channels:
            if channel[1] is not None and len(channel[0]) > 0:
                while len(channel[0]) > 0:
                    channel[1].send(channel[0].popleft())
                channel[1] = None

    def GetOpenChannel(self):
        '''
        Returns the index of a channel that is open.
        '''
        for i, channel in enumerate(self.Channels):
            if channel[1] is None:
                return i
        raise RuntimeError('no channels available')

    def IsChannelOpen(self, index):
        '''
        Returns True if the channel has no receivers.
        '''
        return self.Channels[index][1] is None

    def SendPacket(self, channel, packet):
        '''
        Pickles and sends packet on the specified channel.
        '''
        packet = cpickle.dumps(packet, cpickle.HIGHEST_PROTOCOL)
        packhead = struct.pack('HHB', len(packet), 0, channel)

        buf = self.OutBuffer
        buf.write(packhead)
        buf.write(packet)

        # Senders are a tuple with the outgoing packet and
        # the tasklet channel being used
        #chan = GApp.CreateChannel(1)
        #sender = (outpack, chan)
        #self.Senders.append(sender)
        #sent = chan.receive()
        #return sent

    def ReadPacket(self, channel):
        '''
        Reads a packet from the specified channel. Will block
        until a packet is received.
        '''
        if not self.IsChannelOpen(channel):
            raise RuntimeError('attempted to read on busy channel')
        stchan = stackless.channel()
        stchan.preference = 1
        self.Channels[channel][1] = stchan
        return stchan.receive()

    def SendMessage(self, text):
        self.SendPacket(0, (PT_MESSAGE, text))

    def ReadMessage(self):
        ptype, ptext = self.ReadPacket(0)
        return ptext

    def OnConnected(self):
        GLogger.debug('opened connection: %s', self.addr)

    def OnClosed(self):
        GLogger.debug('closed connection: %s', self.addr)

class CNetServerConnection(CNetConnection):
    def __init__(self, server, index=-1, *args, **kwds):
        CNetConnection.__init__(self, *args, **kwds)
        self.Server = server
        self.Index = index

    def OnConnected(self):
        pass

    def OnClosed(self):
        self.Server.OnConnectionClosed(self)

class CNetListener(object, asyncore.dispatcher):
    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.bind((host, port))
        self.Host = host
        self.Port = port
        self.Connections = []
        self.NextID = 1
        self.listen(1)
        GLogger.debug('created NetServer')
        GLogger.debug('host=%s, port=%s', host, port)

    def handle_accept(self):
        conn, addr = self.accept()
        GLogger.debug('server accepted connection: %s', addr)  
        sconn = CNetServerConnection(self, sock=conn)
        self.AddConnection(sconn)
        self.OnConnectionAccepted(sconn)
    
    def GetNewID(self):
        id = self.NextID
        self.NextID += 1
        return id

    def AddConnection(self, conn):
        if not isinstance(conn, CNetServerConnection):
            raise TypeError()
        i = len(self.Connections)
        conn.Index = i
        self.Connections.append(conn)
        return i

    def RemoveConnection(self, conn):
        if not isinstance(conn, CNetServerConnection):
            raise TypeError()
        if self.Connections[conn.Index] is not conn:
            raise RuntimeError('connection index mismatch')
        ci = conn.Index
        for i in range(ci + 1, len(self.Connections) - 1):
            i.Index -= 1
        del self.Connections[ci]
        conn.Server = None

    def OnConnectionAccepted(self, conn):
        pass

    def OnConnectionClosed(self, conn):
        GLogger.debug('server connection closed: %s', conn.addr)
        self.RemoveConnection(conn)